using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using XEvolve.Repository.Core;

namespace XEvolve.Repository.Internal
{
    internal class MemoryBasedRepository : IRepository
    {
        private readonly Dictionary<Type, Dictionary<Guid, IEntity>> repositories = new Dictionary<Type, Dictionary<Guid, IEntity>>();
        public T Save<T>(T entity) where T : IEntity
        {
            CreateRepositoryIfNotInitialzed<T>();
            if (entity != null)
            {
                var entityType = entity.GetType();
                if (entity.Id == Guid.Empty)
                {
                    entityType.GetProperty("Id", typeof(Guid)).SetValue(entity, Guid.NewGuid(), null);
                    repositories[typeof (T)][entity.Id] = entity;
                }
                if (!repositories[typeof (T)].ContainsKey(entity.Id))
                {
                    repositories[typeof (T)][entity.Id] = entity;
                }
                SaveProperties(entity);
                SaveFields(entity);
            }
            return entity;
        }

        private void SaveFields<T>(T entity)
        {
            var entityType = entity.GetType();
            var fields = entityType.GetFields(BindingFlags.Instance | BindingFlags.Public).Union(entityType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)).Where(fieldInfo => fieldInfo.IsDefined(typeof(SaveToRepositoryAttribute), true));
            foreach (var field in fields)
            {
                var genericMethod = GetType().GetMethod("Save");
                var typedMethod = genericMethod.MakeGenericMethod(GetMethodType(field.FieldType));
                if (field.FieldType.IsArray || field.FieldType.IsGenericType)
                {
                    var values = (IList)field.GetValue(entity) ?? new object[0];
                    foreach (var value in values)
                    {
                        typedMethod.Invoke(this, new[] {value});
                    }
                }
                else
                {
                    typedMethod.Invoke(this, new[] { field.GetValue(entity) });
                }
            }
        }

        private void SaveProperties<T>(T entity)
        {
            var entityType = entity.GetType();
            var properties = entityType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Union(entityType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)).Where(propertyInfo => propertyInfo.IsDefined(typeof(SaveToRepositoryAttribute), true));
            foreach (var property in properties)
            {
                var genericMethod = GetType().GetMethod("Save");
                var typedMethod = genericMethod.MakeGenericMethod(GetMethodType(property.PropertyType));
                if (property.PropertyType.IsArray || property.PropertyType.IsGenericType)
                {
                    var values = (IList)property.GetValue(entity, null) ?? new object[0];
                    foreach (var value in values)
                    {
                        typedMethod.Invoke(this, new[] { value });
                    }
                }
                else
                {
                    typedMethod.Invoke(this, new[] { property.GetValue(entity, null) });
                }
            }
        }

        private static Type GetMethodType(Type type)
        {
            if (type.IsArray)
            {
                return type.GetElementType();
            }
            return type.IsGenericType ? type.GetGenericArguments().First(argument => argument.GetInterface("IEntity", true) != null) : type;
        }

        public bool Delete<T>(T entity) where T : IEntity
        {
            CreateRepositoryIfNotInitialzed<T>();
            return repositories[typeof (T)].Remove(entity.Id);
        }

        public IEnumerable<T> Find<T>(Predicate<T> criteria) where T : IEntity
        {
            CreateRepositoryIfNotInitialzed<T>();
            return repositories[typeof(T)].Values.ToList().Cast<T>().Where(entity => criteria.Invoke(entity));
        }

        public void ClearAll()
        {
            repositories.Clear();
        }

        public void Clear<T>()
        {
            CreateRepositoryIfNotInitialzed<T>();
            repositories[typeof (T)].Clear();
        }

        private void CreateRepositoryIfNotInitialzed<T>()
        {
            if (!repositories.ContainsKey(typeof(T)))
            {
                repositories[typeof(T)] = new Dictionary<Guid, IEntity>();
            }
        }
    }
}